﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.Threading;

namespace SGGWElevator.Controllers
{
    /// <summary>
    /// Controls single elevator
    /// Author: Paweł Iżycki
    /// </summary>
    public class ElevatorController : ControllerBase
    {
        /// <summary>
        /// Elevator controll
        /// </summary>
        ElevatorControl _elevatorControl;

        ///// <summary>
        ///// Elevator instance.
        ///// </summary>
        //Cabin _cabin;

        /// <summary>
        /// Elevator engines, controls elevator movement.
        /// </summary>
        ElevatorEngine _elevatorEngine;

        /// <summary>
        /// Requests container, contains elevators requests to perform.
        /// </summary>
        RequestsController _requestsContainer;

        bool performingRequest;
        /// <summary>
        /// Is elevator performing request in actual time.
        /// </summary>
        public bool IsPerformingRequest
        {
            get
            {
                return this.performingRequest;
            }
            set
            {
                performingRequest = value;

                if (performingRequest) // set to 'performing'
                {
                    //TODO change elevator state
                }
                else // set to 'waiting'
                {
                    //TODO change elevator state
                }
            }
        }


        Direction rideDirection;
        /// <summary>
        /// Elevator ride direction.
        /// </summary>
        public Direction RideDirection
        {
            get { return rideDirection; }
            set
            {
                if (value == Direction.Undifined)
                    throw new ArgumentNullException();
                rideDirection = value;
            }
        }

        /// <summary>
        /// List of sectors. Each sector represents floor bottom and top. 
        /// </summary>
        BiDictionary<Floor, Sector> floorSectors;

        // Constructor
        public ElevatorController(ElevatorControl elevatorControl, ElevatorEngine engine, Floor defaultFloor)
        {
            // Set elevator actual state to 'standby'
            this.IsPerformingRequest = false;

            //// Elevator cabin
            //this._cabin = new Cabin(UniqueIdGenerator.GetUniqueId(), Cabin.DefaultElevatorProperties.DefaultLoadLimit, defaultFloor);

            // Elevator control (GUI)
            if (elevatorControl == null)
                throw new ArgumentNullException();
            else
            {
                this._elevatorControl = elevatorControl;

                // Subscribe for listening to Elevator Callers
                SubscribeToElevatorCallers();

                // Subscribe for listening to Floor Buttons
                _elevatorControl.FloorButtons.FloorSelectedEvent += FloorButtons_FloorSelectedEvent;

                // Create a set of floor starting and ending points.
                floorSectors = CreateFloorSectors();
            }

            // Elevator engine
            if (engine == null)
                throw new ArgumentNullException();
            else
                this._elevatorEngine = engine;

            // Requests container
            _requestsContainer = new RequestsController(this);
            this._requestsContainer.PendingRequestReloaded += requestsContainer_NewPendingRequestSet;
            this._requestsContainer.NewPendingRequestAdded += _requestsContainer_NewPendingRequestAdded;

            // Subscribe to itself
            PerformingRequestEnded += ElevatorController_PerformingRequestEnded;

            _dbLogger.DebugMode = ControllerProperties.DEBUG_MODE;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="pendingRequest"></param>
        void _requestsContainer_NewPendingRequestAdded(object sender, Request pendingRequest)
        {
            // Turn on requested floor button
            _elevatorControl.FloorButtons.SwitchOnOffFloor(pendingRequest.RequestedFloor, true);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="rquestedFloor"></param>
        void FloorButtons_FloorSelectedEvent(object sender, Floor requestedFloor)
        {
            _logger.WriteLine(
                String.Format("caller_CallerClickedEvent - Floor button has been clicked! Selected floor: {0}", requestedFloor.ToString()));

            // Set starting floor
            Floor startingFloor = GetFloor(_elevatorControl.ElevCabin.GetPosition());

            // Set direction
            Direction direction = Direction.Undifined;
            if (requestedFloor != startingFloor)
            {
                if (requestedFloor.IsAfter(startingFloor))
                    direction = Direction.Up;
                else
                    direction = Direction.Down;
            }
            else
                direction = _elevatorControl.InfoPanel.ActualDirection; // Remain direction unchanged            

            // Create new request
            Request request = new Request(UniqueIdGenerator.GetUniqueId(), startingFloor, requestedFloor, direction);

            if (!this.IsPerformingRequest && startingFloor == requestedFloor)
                PerformShortRequest(request);
            else
                _requestsContainer.Add(request);

        }

        /// <summary>
        /// Performs short requests such as opening door on the same floor.
        /// Ta metoda powinna być używana wtedy, kiedy winda zostanie wezwana na piętro, na którym aktualnie już jest.
        /// </summary>
        /// <param name="request"></param>
        void PerformShortRequest(Request request)
        {
            _logger.WriteLine("ElevatorController.PerformShortRequest - Start performing...");

            if (request.RequestedFloor != request.StartingFloor)
                throw new Exception("Starting floor is not the same as requested floor, use another method!");

            // TODO check is elevator moving

            // if door is closed, open

            // if door is open, do nothing or add additional time to open time

            if (PerformingRequestEnded != null)
                PerformingRequestEnded(this, request);

            _logger.WriteLine("ElevatorController.PerformShortRequest - End performing...");
        }

        /// <summary>
        /// Called when performing request is finished.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="finishedRequest"></param>
        void ElevatorController_PerformingRequestEnded(object sender, Request finishedRequest)
        {
            _dbLogger.WriteLine("ElevatorController_PerformingRequestEnded - Turning off caller and floor button...");

            _elevatorControl.UseDispatcher(
                new Action(delegate()
                    {
                        // Turn off caller
                        _elevatorControl.ElevatorCallers.Find(x => x.CallersFloor == finishedRequest.RequestedFloor).TurnOffCaller();

                        // Turn off button
                        _elevatorControl.FloorButtons.SwitchOnOffFloor(finishedRequest.RequestedFloor, false);
                    }));
        }

        /// <summary>
        /// Creates set of floor sectors. Each sectors ending is floors top or bottom. 
        /// </summary>
        /// <returns></returns>
        BiDictionary<Floor, Sector> CreateFloorSectors()
        {
            BiDictionary<Floor, Sector> result = new BiDictionary<Floor, Sector>();

            double precision = Configuration.CALCULATING_FLOOR_PRECISION;
            int floorCount = 0;
            foreach (Floor floor in ExtFloor.Floors)
            {
                double startPoint = floorCount * Configuration.FLOOR_HEIGHT + precision;
                double endPoint = (floorCount + 1) * Configuration.FLOOR_HEIGHT;
                Sector floorSector = new Sector(startPoint, endPoint);
                result.Add(floor, floorSector);
                floorCount += 1;
            }

            return result;
        }

        /// <summary>
        /// Calculates which floor corresponds to given cabins position.
        /// </summary>
        /// <param name="position">Cabins position in pixels</param>
        /// <returns></returns>
        Floor GetFloor(double cabinPosition)
        {
            foreach (Floor floor in ExtFloor.Floors)
            {
                var segment = floorSectors.GetByFirst(floor);

                _dbLogger.WriteLine(string.Format("ElevatorController.GetFloor - cabinPosition: {0}, segment.StartPoint {1}, segment.EndPoint: {2}", cabinPosition, segment.StartPoint, segment.EndPoint));

                if (cabinPosition >= segment.StartPoint && cabinPosition <= segment.EndPoint)
                    return floor;
            }

            return Floor.Undifined;
        }

        /// <summary>
        /// Updates Floor in info panel
        /// </summary>
        void UpdateElevatorInfoPanelFloor()
        {
            _elevatorControl.UseDispatcher(new Action(delegate()
            {
                // Get cabins actual position
                var cabinPosition = _elevatorControl.ElevCabin.GetPosition();
                _dbLogger.WriteLine("UpdateElevatorInfoPanelFloor - Setting floor in Info Panel...");
                Floor actualFloor = GetFloor(cabinPosition);
                _dbLogger.WriteLine(string.Format("UpdateElevatorInfoPanelFloor - Actual floor: {0}", actualFloor));
                if (actualFloor != Floor.Undifined)
                    _elevatorControl.InfoPanel.ActualFloor = actualFloor;
            }));
        }


        /// <summary>
        /// Updates direction arrow in info panel.
        /// </summary>
        void UpdateElevatorInfoPanelDirection(Request request)
        {
            _elevatorControl.UseDispatcher(new Action(delegate()
            {
                Direction direction = Direction.Undifined;
                var cabinPosition = _elevatorControl.ElevCabin.GetPosition();
                Floor actualFloor = GetFloor(cabinPosition);
                if (actualFloor != Floor.Undifined)
                {
                    if (request.StartingFloor == request.RequestedFloor)
                        return; // if the floor is the same, remain the same direction
                    else if (request.RequestedFloor.IsAfter(actualFloor))
                        direction = Direction.Up;
                    else
                        direction = Direction.Down;

                    // Change direction
                    _elevatorControl.InfoPanel.ActualDirection = direction;
                }
            }));
        }

        /// <summary>
        /// Called when new request appears in request container. Makes Cabin start moving.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        void requestsContainer_NewPendingRequestSet(object sender, Request pendingRequest)
        {
            StartPerformingRequest(pendingRequest);
        }

        /// <summary>
        /// Subscribes controller for listning all elevatorControls callers.
        /// </summary>
        private void SubscribeToElevatorCallers()
        {
            foreach (var caller in _elevatorControl.ElevatorCallers)
            {
                caller.CallerClickedEvent += caller_CallerClickedEvent;
            }
        }

        /// <summary>
        /// Handles clicking on the elevator caller
        /// </summary>
        /// <param name="sender">Sending elevator caller</param>
        /// <param name="requestedFloor">Requested floor</param>
        /// <param name="direction">Requested direction</param>
        void caller_CallerClickedEvent(object sender, Floor requestedFloor, Direction direction)
        {
            _logger.WriteLine(
                String.Format("caller_CallerClickedEvent - Elevator caller has been clicked! Selected floor: {0}, selected direction: {1}", requestedFloor.ToString(), direction.ToString()));

            // Create new request
            Floor startingFloor = GetFloor(_elevatorControl.ElevCabin.GetPosition());
            Request request = new Request(UniqueIdGenerator.GetUniqueId(), startingFloor, requestedFloor, direction);

            // If requested floor is equal to starting floor...
            if (request.RequestedFloor == request.StartingFloor)
                PerformShortRequest(request);
            else
                _requestsContainer.Add(request);
        }

        #region Performing requests

        /// <summary>
        /// Actually performed request.
        /// </summary>
        Request performedRequest = null;

        /// <summary>
        /// Timer, used in updating cabins position.
        /// </summary>
        System.Timers.Timer timer = new System.Timers.Timer();
        //object locker = new object();
        Thread requestThread;

        /// <summary>
        /// Called when actually performed request ends.
        /// </summary>
        public event PerformingRequestEndedEventHandler PerformingRequestEnded;
        public delegate void PerformingRequestEndedEventHandler(object sender, Request finishedRequest);

        /// <summary>
        /// Starts performing pending request.
        /// </summary>
        /// <param name="request"></param>
        void StartPerformingRequest(Request request)
        {
            if (this.IsPerformingRequest)
                return;

            // Set ride direction
            rideDirection = request.Direction;

            _logger.WriteLine("UpdateElevatorInfoPanelFloor - Attempting to start new thread with request...");
            // Start new thread with performed request            

            requestThread = new Thread(new ThreadStart(delegate()
            {
                _logger.WriteLine("UpdateElevatorInfoPanelFloor - Starting...");

                this.IsPerformingRequest = true;

                performedRequest = request;

                // === Start scenario === 

                // Update direction arrow in info panel
                UpdateElevatorInfoPanelDirection(performedRequest);

                // Close door
                _elevatorControl.ElevCabin.CloseDoor();

                // Give some time to close the door
                Suspend(ControllerProperties.CLOSING_DURATION);

                // Start moving cabin
                timer.Interval = ControllerProperties.UPDATE_INTERVAL;
                timer.Elapsed += UpdateElevatorLocation;
                timer.Enabled = true;
            }));

            requestThread.Name = string.Format(request.ToString());

            requestThread.Start();
            _logger.WriteLine("UpdateElevatorInfoPanelFloor === Performing request has started! ===");
        }



        /// <summary>
        /// Updates cabin position.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void UpdateElevatorLocation(object sender, ElapsedEventArgs e)
        {
            // Updates cabin position
            _elevatorEngine.GoToFloor(ElevatorEngine.GetFloorPosition(this.performedRequest.RequestedFloor));
            _elevatorControl.ElevCabin.SetPosition(_elevatorEngine.ActualPosition);

            // Updates floor at info panel 
            UpdateElevatorInfoPanelFloor();

            _dbLogger.WriteLine(String.Format("UpdateElevatorLocation - Aktualna pozycja: {0}, Aktualna predkosc: {1}", _elevatorEngine.ActualPosition, _elevatorEngine.ActualVelocity));

            // Stop condition
            if (Math.Abs(_elevatorEngine.ActualPosition - ElevatorEngine.GetFloorPosition(this.performedRequest.RequestedFloor)) < 1)
                EndPerformingRequest();
        }

        /// <summary>
        /// Detaches updating cabins location from the timer and changes elevator state.
        /// Should be called on every request ending.
        /// </summary>
        void EndPerformingRequest()
        {
            _logger.WriteLine("EndPerformingRequest - Ending performing request...");
            timer.Elapsed -= UpdateElevatorLocation;
            _elevatorControl.ElevCabin.OpenDoor();
            Suspend(ControllerProperties.CLOSING_DURATION);

            this.IsPerformingRequest = false;
            _logger.WriteLine("EndPerformingRequest === Performing request has finished! ===");

            if (PerformingRequestEnded != null)
                PerformingRequestEnded(this, performedRequest);

        }

#if FALSE // Alternatywne podejście do aktualizaowania pozycji windy
        Thread thread = new Thread(new ThreadStart(delegate()
        {
            this.IsPerformingRequest = true;

            for (int i = 0; i < 200; i++)
            {
                // uaktualnia na jakiej pozycji powwinna być kabina
                _elevatorEngine.GoToFloor(ElevatorEngine.GetFloorPosition(_requestsContainer.PendingRequest.RequestedFloor));
                System.Diagnostics.Debug.WriteLine(String.Format("Aktualna pozycja: {0}, Aktualna predkosc: {1}", _elevatorEngine.AcutalPosition, _elevatorEngine.ActualVelocity));

                _elevatorControl.ElevCabin.SetPosition(_elevatorEngine.AcutalPosition);

                if (Math.Abs(_elevatorEngine.AcutalPosition - ElevatorEngine.GetFloorPosition(Floor.Three)) < 1) //czyli dojechane na miejsce, zatrzymaj
                    break;
            }
            this.IsPerformingRequest = false;
        }));

        thread.Start();
#endif

        /// <summary>
        /// Suspends actual thread for given amount of time.
        /// </summary>
        /// <param name="suspendTime"></param>
        public void Suspend(long suspendTime)
        {
            _logger.WriteLine(String.Format("Wait for {0} miliseconds.", suspendTime));
            DateTime dt = DateTime.Now;
            dt = dt.AddMilliseconds(suspendTime);
            while (DateTime.Now.Ticks < dt.Ticks) ;
        }

        /// <summary>
        /// Creates new thread in which holds for given amount of time and then releases executing futher (given) instructions. 
        /// </summary>
        /// <param name="suspendTime"></param>
        /// <param name="nextAction"></param>
        public void Suspend(long suspendTime, Delegate nextAction, params object[] args)
        {
            Thread t = new Thread(new ThreadStart(delegate
            {
                Suspend(suspendTime);
                nextAction.DynamicInvoke(args);
            }));
            t.Start();
        }

        #endregion

        class ControllerProperties
        {
            // Times in miliseconds
            public readonly static long UPDATE_INTERVAL = Configuration.CABIN_POSITION_UPDATE_INTERVAL;
            public readonly static long OPENING_DURATION = Configuration.OPENING_DOOR_DURATION;
            public readonly static long CLOSING_DURATION = Configuration.CLOSING_DOOR_DURATION;

            public const bool DEBUG_MODE = false;
        }
    }
}
